iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
JavaScript

Signal API in Angular系列 第 29

Day 29 - 測試 Signals

  • 分享至 

  • xImage
  •  

測試 signals 是軟體開發中非常重要的一部分,但由於時間限制,它很容易被開發團隊忽略。

在應用程式中使用 signalscomputed signalssignal inputseffect 時,負責的團隊會編寫測試案例來驗證其正確性。

這是我第一次寫程式碼來測試 signals;因此,我在寫這篇文章時也學到了新東西。

將 signal 和 computed signal 加入到 AppService

import { computed, Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class AppService {
  #counter = signal(0);

  value = computed(() => this.#counter());

  increase(num = 1) {
    this.#counter.update((prev) => prev + num);
  }

  decrease(num = 1) {
    this.#counter.update((prev) => prev - num);
  }

  reset() {
    this.#counter.set(0);
  }
}

這是一個偏好問題。我將 #counter signal 和 value computed signal 封裝在 AppService 服務中,但邏輯也可以在 AppComponent 組件中實現,因為它很簡單。

建立子組件

import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [],
  template: `
    <p>Child works!</p>
    <p data-testId="count">Count: {{ count() }}</p>
    <p data-testId="double">Double: {{ double() }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
  count = input(0);
  double = computed(() => this.count() * 2);
} 

ChildComponent 組件由 count signal input 和 double computed signal 組成。

將AppService和ChildComponent注入到AppComponent中

import { ChangeDetectionStrategy, Component, effect, inject } from '@angular/core';
import { AppService } from './app.service';
import { ChildComponent } from './child/child.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <h1>Hello, {{ title }}</h1>
    <div>
      <p id="value">Value: {{ appService.value() }}</p>
      <button id="increase" (click)="increase(1)">Add 1</button>
      <button id="increase2" (click)="increase(2)">Add 2</button>
      <button id="decrease" (click)="decrease(1)">Decrease</button>
      <button id="decrease2" (click)="decrease(2)">Decrease 2</button>
      <button id="reset" (click)="reset()">Reset</button>  
      <app-child [count]="appService.value()" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  title = 'day29-signal-testing';
  appService = inject(AppService);

  increase(num = 1) {
    this.appService.increase(num);
  }

  decrease(num = 1) {
    this.appService.decrease(num);
  }

  reset() {
    this.appService.reset();
  }
}

建立可重複使用的測試實用函數

export const buttonClick = <T>(el: DebugElement, fixture: ComponentFixture<T>,
   target: HTMLElement, expected: string) => {   

   el.nativeElement.click();
   fixture.detectChanges();
   expect(target.textContent).toBe(expected);
}

export const getElement = <T>(fixture: ComponentFixture<T>, key: string): DebugElement => {
   return fixture.debugElement.query(By.css(key));
}

test/button-test.util.ts 檔案中新增兩個測試用例將重複呼叫的實用函數。 getElement 函數透過 fixtureCSS 選擇器 查詢 DebugElementbuttonClick 函數使用 HTML 元素執行單擊,觸發 change detection,並比較元素文字是否與預期結果相同。

el.nativeElement.click();
fixture.detectChanges();

該元素執行單擊,然後發生 change detection。

expect(target.textContent).toBe(expected);

將元素文字與預期結果進行比較。如果比較為正確的,則測試通過。否則,測試失敗,我需要修正它。

在 AppComponent spec 檔中設定初始化程式碼

// app.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ChildComponent } from './child/child.component';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent, ChildComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
  });
}

我們可以在 app.component.spec.ts 檔案中找到測試案例。 beforeEach 函數在每個測試案例之前執行,以編譯 AppComponent 組件、初始化 fixture 變數並觸發 change detection。

測試 signal 和 computed signal

it('should increase the counter by 1', () => {  
    const value: HTMLParagraphElement = getElement(fixture, '[id="value"]').nativeElement;
   expect(value.textContent).toBe('Value: 0');
   const el = getElement(fixture, '[id="increase"]');     
   buttonClick(el, fixture, value, 'Value: 1');
});

第一個測試案例驗證 #counter signal 增加 1 並且 value computed signal 顯示正確的值。 第一個 expect 語句驗證 paragrah 元素 'Value: 0'。 呼叫可重複使用實用函數後,測試程式碼更具可讀性。

const el = getElement(fixture, '[id="increase"]');     
buttonClick(el, fixture, value, 'Value: 1');

然後,測試用例查詢 "Add 1" 按鈕並將其傳遞給 buttonClick 函數。此函數點擊按鈕,在執行 change detection 後更新 signal,並驗證元素文字為 'Value:1'。

it('should increase the counter by 2 after 2 button clicks', () => {  
    const value: HTMLParagraphElement = getElement(fixture, '[id="value"]').nativeElement;
   expect(value.textContent).toBe('Value: 0');
    const el = getElement(fixture, '[id="increase"]');
   buttonClick(el, fixture, value, 'Value: 1');
   buttonClick(el, fixture, value, 'Value: 2');
});

此測試案例驗證點擊兩次按鈕後 paragraph 元素是否顯示正確的值。 點擊按鈕並進行 change detection。點擊第一次按鈕後,paragraph 元素顯示 'Value:1'。 再次按一下相同按鈕,會出現另一個 change detection。此元素顯示預期值 'Value:2'。

it('should update the child component', () => {
    const value: HTMLParagraphElement = getElement(fixture, '[id="value"]').nativeElement;
   expect(value.textContent).toBe('Value: 0');
   const el = getElement(fixture, '[id="increase2"]');
   buttonClick(el, fixture, value, 'Value: 2');

   const count: HTMLParagraphElement = getElement(fixture, '[data-testId="count"]').nativeElement;
   const double: HTMLParagraphElement = getElement(fixture, '[data-testId="double"]').nativeElement;
   expect(count.textContent).toBe('Count: 2');
   expect(double.textContent).toBe('Double: 4');
});

ChildComponent 組件是 AppComponent 組件的子元件。我想驗證 input 更新時子組件顯示正確的值。此測試案例查詢 "Add 2" 按鈕,模擬按鈕單擊和 change detection。 paragraph元素顯示正確的 'Value:2'。

const count: HTMLParagraphElement = getElement(fixture, '[data-testId="count"]').nativeElement;
expect(count.textContent).toBe('Count: 2');

第一行查詢 ChildComponent 顯示 count signal input 的 paragraph 元素。按鈕點擊後 input 變為2;因此,該元素顯示 'Value:2'。 expect 語句驗證元素文字是否與預期結果相同。

const count: HTMLParagraphElement = getElement(fixture, '[data-testId="double"]').nativeElement;
expect(count.textContent).toBe('Count: 4');

第一行查詢 ChildComponent 顯示 double computed signal 的 parapgrah 元素。按鈕點擊後輸入變為2;因此, double computed signal 重新計算為 4。 expect 語句驗證元素文字是否與預期結果相同。

在 ChildComponent spec 檔中設定初始化程式碼

// child.component.spec.ts

let fixture: ComponentFixture<ChildComponent>;

beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ChildComponent]
    })
    .compileComponents();

    fixture = TestBed.createComponent(ChildComponent);
    fixture.detectChanges();
});

child.component.spec.ts 中,我們初始化 fixturecomponent 變數來測試 signal input。

it('should update the signal input', () => {
   const count: HTMLParagraphElement = getElement(fixture, '[data-testId="count"]').nativeElement;
   const double: HTMLParagraphElement = getElement(fixture, '[data-testId="double"]').nativeElement;

    expect(count.textContent).toBe('Count: 0');  
    expect(double.textContent).toBe('Double: 0');

    fixture.componentRef.setInput('count', 1);  
    fixture.detectChanges();

    expect(count.textContent).toBe('Count: 1');
    expect(double.textContent).toBe('Double: 2');
});

signal input為 0,因此兩個段落元素最初都顯示 'Count: 0' 和'Double: 0'。

fixture.componentRef.setInput('count', 1);  
fixture.detectChanges();

我使用 componentRef.setInputcount input 設為 1,並觸發 change detection。

expect(count.textContent).toBe('Count: 1');
expect(double.textContent).toBe('Double: 2');

count 段落元素顯示 'Value:1'。 double computed signal 變為 2,因此段落元素顯示 'Double: 2'。

我無法讓 effect 測試用例發揮成功;因此,我不會在這篇文章中包含任何 effect 的測試案例。

結論:

  • 可測試 signalcomputed()input() 和 effect。
  • 執行 UI 操作後,請記住模擬 change detection 以更新訊號 (signal)。然後,編寫CSS來查詢HTML元素以驗證結果。
  • 重構測試案例,將通用程式碼提取到函數 (utility function) 中。

鐵人賽的第 29 天到此結束。

參考:


上一篇
Day 28 - 使用 Facade Pattern 從 Signal 遷移到 State Management Library
下一篇
Day 30 - Angular 和 Signal 的未來
系列文
Signal API in Angular39
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言